"""Simple Wiki - это вики движок, отображающий файловую структуру в веб-энциклопедию; поддерживает статьи в форматах .html, .md. Simple Wiki написана на языке python, поддерживает гибкое расширение и стилизацию."""
from __future__ import annotations

import json
import mimetypes
import random
import re
from dataclasses import dataclass
from typing import Callable, List, Optional

import chardet
import jinja2
import typer
from attributedict.collections import AttributeDict
from markdown import markdown

from engine.responses import DataResponse, FileResponse, NotFoundResponse, Response
from engine.webserver import serve
from engine.path import Path

__version__ = '0.1.2'
templates = jinja2.Environment(loader=jinja2.FileSystemLoader('templates'), autoescape=['html'])
mimetypes.add_type('text/markdown', '.md', strict=False)
mimetypes.add_type('font/woff', '.woff', strict=False)
mimetypes.add_type('font/woff2', '.woff2', strict=False)
config = AttributeDict(json.loads((Path.cwd() / 'config.json').read_text()))
parsers = {
	'.md': lambda text: markdown(text, extensions=['extra', 'mdx_math', 'admonition', 'toc', 'wikilinks'] + config['markdown_extensions'], extension_configs={
		'extra':     {
			'footnotes':   {
				'UNIQUE_IDS': True
			},
			'fenced_code': {
				'lang_prefix': 'lang-'
			}
		},
		'mdx_math':  {
			'enable_dollar_delimiter': True,
			'add_preview':             True
		},
		'wikilinks': {
			# 'pattern':   r'\[\[([\w0-9_ -/#]+)\]\]',
			'end_url':   '',
			'build_url': make_internal_link
		}
	}),
}

# region default images

default_icon = ''

default_logo = ''


# endregion


@dataclass
class WikiLink:
	name: str
	url: str
	page: bool

	@staticmethod
	def from_path(wiki_link_path: Path, wiki_root_path: Path) -> WikiLink:
		"""
		Generate link from file system path.
		"""
		if not is_relative_to(wiki_link_path, wiki_root_path):
			raise ValueError(f'Wiki path {wiki_link_path} must be relative to root {wiki_root_path}.')
		if not is_page(wiki_link_path) and not is_section(wiki_link_path):
			raise ValueError(f'Wiki path must point to page or section not to resource.')
		page = not is_section(wiki_link_path)
		if page:
			wiki_link_path = wiki_link_path.parent / wiki_link_path.with_suffix('')
		return WikiLink(name=wiki_link_path.name, url='/' + str(wiki_link_path.relative_to(wiki_root_path)), page=page)

	def __repr__(self):
		return (self.page and 'Page' or 'Section') + f': {self.name} ({self.url})'

	def __str__(self):
		return repr(self)


def validate_port(port: int) -> int:
	"""
	Check whether port index is valid.
	"""
	if not (0 < port <= 65535):
		raise typer.BadParameter('Port value must be in range from 1 to 65535.')
	return port


def show_version(value: bool):
	"""
	Print version info and exit.
	"""
	if value:
		typer.echo(f"Simple Wiki {__version__}")
		raise typer.Exit()


def render_template(name: str, **rendering_arguments) -> str:
	return templates.get_template(name).render(config=config, **rendering_arguments)


def same_paths(*paths: Path) -> bool:
	"""
	Whether the given paths point to same location.
	"""
	if len(paths) < 2:
		raise ValueError('At least two paths are reqired for comparison.')
	pattern = paths[0].absolute()
	return all([x.absolute() == pattern for x in paths[1:]])


def make_wiki_path(path: Path, wiki_root_path: Path) -> str:
	"""
	Convert path to wiki page url.
	"""
	return str(path.relative_to(wiki_root_path).with_suffix(''))


def make_internal_link(label, prefix, postfix):
	wiki_path = Path.cwd() / 'wiki'
	label = re.sub(r'\s*/\s*', '/', label)
	match = re.search(r'\s*#.*', label)
	if match:
		anchor = re.sub(r'\s*#\s*', '#', match.group())
		label = label[:match.start(0)]
	else:
		anchor = ''
	found = list(wiki_path.rglob(f'{label}*'))
	if len(found) != 0:
		name = found[0].name.split('.')[0]
		label = str(found[0].relative_to(wiki_path))
	else:
		name = label.split('/')[-1]
	# if len(anchor) != 0:
	# 	name = anchor[1:]
	return f'{prefix}{label}{anchor}{postfix}', name


def find_paths(path: Path, pattern: str, filter: Callable[[Path], bool]) -> List[Path]:
	"""
	Glob pattern relatively to path and filter results by predicate.
	"""
	return [x for x in sorted(path.glob(pattern), key=str) if filter(x)]


def generate_side_block(links: List[WikiLink], label: str = '', *, sort: bool = True) -> str:
	"""
	Generate block of links markup for side bar.
	:param label: label of block.
	:param links: list of label, url pairs.
	:param sort: whether to sort links by name (label).
	"""
	if sort:
		links = sorted(links, key=lambda link: link.name)
	return render_template('side_block.html', label=label, links=links)


def is_section(wiki_path: Path) -> bool:
	return wiki_path.exists() and wiki_path.is_dir()


def is_page(wiki_path: Path) -> bool:
	page_types = ['text', 'pdf']
	mime = mimetypes.guess_type(wiki_path, strict=False)[0]
	return wiki_path.exists() and wiki_path.is_file() and mime is not None and any([x in mime for x in page_types])


def is_resource(path: Path) -> bool:
	return is_relative_to(path, Path.cwd() / 'resources')


def is_wiki_page(path: Path) -> bool:
	return is_relative_to(path, Path.cwd() / 'wiki')


def is_search_query(path: Path) -> bool:
	cwd = Path.cwd()
	search_path = cwd / 'search'
	# noinspection PyUnboundLocalVariable
	return not same_paths(path, search_path) and is_relative_to(path, search_path) and len(parents := path.relative_to(search_path).parents) == 1 and same_paths(search_path, search_path / parents[0])


def get_query(search_query: Path) -> Optional[str]:
	if is_search_query(search_query):
		return search_query.name


def generate_sidebar(current_wiki_path: Path) -> str:
	"""
	Generate wiki side bar menu.
	"""
	wiki_path = Path.cwd() / 'wiki'
	main_links = [WikiLink(name='Заглавная страница', url='/', page=False)]
	if not same_paths(current_wiki_path, wiki_path):
		# current section
		if is_relative_to(current_wiki_path.parent, wiki_path) and not same_paths(current_wiki_path.parent, wiki_path):
			main_links.append(WikiLink.from_path(current_wiki_path.parent, wiki_path))
		# supsection
		if not is_section(current_wiki_path) and is_relative_to(current_wiki_path.parent.parent, wiki_path) and not same_paths(current_wiki_path.parent.parent, wiki_path):
			main_links.append(WikiLink.from_path(current_wiki_path.parent.parent, wiki_path))
	main_block = generate_side_block(main_links, sort=False)
	search_section = current_wiki_path
	if not is_section(current_wiki_path):
		search_section = search_section.parent
	siblings = [WikiLink.from_path(x, wiki_path) for x in find_paths(search_section, '*.*', lambda x: is_page(x) and not same_paths(x, current_wiki_path))]
	if len(siblings) == 0:
		siblings_block = ''
	else:
		if len(siblings) > 10:
			siblings = random.sample(siblings, 10)
		siblings_block = generate_side_block(siblings, 'Статьи в разделе')
	subsections = [WikiLink.from_path(x, wiki_path) for x in find_paths(search_section, '*', is_section)]
	if len(subsections) == 0:
		subsections_block = ''
	else:
		if len(subsections) > 10:
			subsections = random.sample(subsections, 10)
		subsections_block = generate_side_block(subsections, 'Подразделы')
	return main_block + siblings_block + subsections_block


def generate_page(content: str, current_wiki_path: Path) -> str:
	"""
	Generate page markup for content markup part.
	"""
	resources_root = Path('../resources')
	icon_path = resources_root / config['icon']
	if icon_path.exists() and icon_path.is_file():
		icon_path = f'/{config["icon"]}'
	else:
		icon_path = default_icon
	logo_path = resources_root / config['logo']
	if logo_path.exists() and logo_path.is_file():
		logo = logo_path.read_text(encoding='utf-8')
	else:
		logo = f'<a href="/" title="Перейти на заглавную страницу" style="background-position:center center;background-repeat:no-repeat;display:block;width:10em;height:160px;text-decoration:none;background-image:url({default_logo});background-size:contain"></a>'
	return render_template('page.html', logo=logo, content=content, sidebar=generate_sidebar(current_wiki_path), icon=icon_path)


def generate_section_content(path: Path) -> str:
	"""
	Generate markup for whole section.
	"""
	if not is_section(path):
		raise ValueError(f'{path} is not a section.')
	wiki_path = Path.cwd() / 'wiki'
	links = [WikiLink.from_path(x, wiki_path) for x in path.glob('*') if is_page(x) or is_section(x)]
	return render_template('section.html', section=path.name, subsections=[x for x in links if not x.page], pages=[x for x in links if x.page])


def match(query, text) -> bool:
	"""
	Match test with query.
	"""
	if len(query) < 3 or len(text) == 0:
		return False
	return query.lower() in text.lower()


def generate_search_results(query) -> str:
	"""
	Generate search results content markup.
	"""
	query = query.strip()
	wiki_path = Path.cwd() / 'wiki'
	results = []
	for path in wiki_path.rglob('*'):
		if not is_page(path) and not is_section(path):
			continue
		if match(query, str(path.relative_to(wiki_path))):
			results.append(path)
		elif is_page(path):
			mime = mimetypes.guess_type(path, strict=False)[0]
			if mime is not None and 'text' in mime:
				content = path.read_bytes()
				try:
					content = content.decode(chardet.detect(content)['encoding'])
					if match(query, content):
						results.append(path)
				except UnicodeDecodeError:
					pass
	return render_template('search_results.html', query=query, found=[WikiLink.from_path(x, wiki_path) for x in results])


def route(requested_path: Path) -> Optional[Path]:
	cwd = Path.cwd()
	if is_search_query(requested_path):
		return requested_path
	path = cwd / 'wiki' / requested_path.relative_to(cwd)
	if not same_paths(path, cwd / 'wiki'):
		for file in path.parent.glob(f'{path.name}.*'):
			return file
	if path.exists() and path.is_dir():
		return path
	else:
		path = cwd / 'wiki' / requested_path.relative_to(cwd)
		if path.exists() and path.is_file():
			return path
		path = cwd / 'resources' / requested_path.relative_to(cwd)
		if path.exists() and path.is_file():
			return path
	return None


def response(requested_path: Path) -> Response:
	query = get_query(requested_path)
	if query is not None:
		return DataResponse(generate_page(generate_search_results(query), requested_path).encode('utf-8'), 'text/html')
	elif is_resource(requested_path):
		return FileResponse(requested_path)
	elif is_wiki_page(requested_path):
		if is_section(requested_path):
			content = generate_section_content(requested_path)
		elif requested_path.exists() and requested_path.is_file():
			mime = mimetypes.guess_type(requested_path, strict=False)[0]
			if mime is None or 'text' not in mime:
				return FileResponse(requested_path)
			content = requested_path.read_bytes()
			try:
				content = content.decode(chardet.detect(content)['encoding'])
			except UnicodeDecodeError:
				return FileResponse(requested_path)
			content_type = requested_path.suffix
			if content_type in parsers:
				content = parsers[content_type](content)
		else:
			return NotFoundResponse()
		return DataResponse(generate_page(content, requested_path).encode('utf-8'), 'text/html')
	else:
		return NotFoundResponse()


def cli(interface: str = typer.Option(config['interface'], '--interface', '-i', help='Interface IP v4 address or resolvable name (like "127.0.0.1" or "localhost") on which to serve wiki. Default value is "0.0.0.0" for all connected interfaces. Overwrites config.json.'), port: int = typer.Option(config['port'], '--port', '-p', help='Port on which to serve wiki. Default value is 80 for all connected interfaces. Overwrites config.json.', callback=validate_port),
        version: Optional[bool] = typer.Option(None, "--version", callback=show_version, is_eager=True, help=show_version.__doc__), debug: bool = typer.Option(False, '--debug', help='Print more information about errors.')):
	"""
	Run wiki server.
	"""
	try:
		serve(interface=interface, port=port, router=route, handle=response)
	except Exception as error:
		typer.secho(str(error), fg=typer.colors.RED, err=True)
		if debug:
			raise
		typer.Exit(-1)


def main():
	typer.run(cli)


if __name__ == "__main__":
	main()
